Scripts/it

Tutorial
Argomento
Scripting
Livello di difficoltà
Base
Tempo di esecuzione
Autori
onekk Carlo
Versione di FreeCAD
0.19
Files di esempio
Vedere anche
Nessuno

Introduzione

Per Scripting intendiamo la creazione di oggetti topologici mediante l'uso dell'interprete Python interno di FreeCAD. FreeCAD può essere usato come "ottimo" sostituto di OpenSCAD, principalmente perché possiede un vero interprete Python, questo vuol dire che ha un intero linguaggio di programmazione "a bordo", quasi tutto quello che si può realizzare con l'interfaccia grafica è possibile realizzarlo anche attraverso uno script Python.

Purtroppo le informazioni che riguardano lo scripting nella documentazione di FreeCAD, e anche in questo wiki sono disperse e mancano di una uniformità di "scrittura" e molte di esse sono spiegate in maniera troppo tecnica.

Per iniziare

Il primo ostacolo ad un semplice approccio allo scripting deriva dal fatto che non esiste un modo per accedere direttamente all'editor Python interno a FreeCAD, con un comando di menù od un'icona nella barra degli strumenti, sapendo però che FreeCAD apre un file con estensione .py nell'editor Python interno, il trucco più semplice è quello di creare usando il proprio editor di testo preferito, un file e poi aprirlo in FreeCAD con File → Apri.

Per fare le cose con un minimo di stile, il file deve essere scritto con un certo ordine. L'editor di FreeCAD possiede una buona "Evidenziazione di Sintassi" che manca a molti editor come Windows Notepad o altri editor di Linux di base, per cominciare basta scrivere queste poche righe:

"""filename.py

   A short description of what the script does

"""

Salvatele con un nome significativo e con estensione .py e caricate il file ottenuto in FreeCAD, con il comando File → Apri.

Un esempio minimale che contiene tutto quanto necessario per uno script è mostrato in questa porzione di codice, che potete usare come modello per quasi ogni vostro futuro script:

"""filename.py

   First FreeCAD Script

"""

import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui

DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)

ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)

# Helper function

def set_view():
    """Rearrange View."""
    if not FreeCAD.GuiUp:
        return
    doc = FreeCADGui.ActiveDocument
    if doc is None:
        return
    view = doc.ActiveView
    if view is None:
        return
    # Check if the view is a 3D view:
    if not hasattr(view, "getSceneGraph"):
        return
    view.viewAxometric()
    view.fitAll()

Nel codice qui sopra sono presenti alcuni trucchi:

Cominciamo con un piccolo script che fa un piccolo lavoro, ma mostra la potenza di questo approccio.

# Script functions

def my_box(name, len, wid, hei):
    """Create a box."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    DOC.recompute()

    return obj_b

# objects definition

obj = my_box("test_cube", 5, 5, 5)

set_view()

Scrivete sopra le righe di codice dopo # Script functions e premete la freccia verde nella Barra degli strumenti Macro.

Accadranno alcune magie, si apre un nuovo documento chiamato "Wiki_example" e si visualizza un Cubo nella Vista 3D, che dovrebbe assomigliare all'immagine qui sotto.

Test cube

Qualcosa di più

Niente di eccezionale? Vero, ma da qualcosa dobbiamo pure incominciare, possiamo fare la stessa cosa con un Cilindro, aggiungete queste linee dopo la funzione my_box() e prima della riga # objects definition.

def my_cyl(name, ang, rad, hei):
    """Create a Cylinder."""
    obj = DOC.addObject("Part::Cylinder", name)
    obj.Angle = ang
    obj.Radius = rad
    obj.Height = hei

    DOC.recompute()

    return obj

Anche qui nulla di eccezionale. Notiamo alcune cose nella costruzione del codice:

Ora cosa dobbiamo fare con questi oggetti?

Introduciamo ora le operazioni booleane. Un esempio per cominciare, mettendo queste linee dopo my_cyl, si crea una funzione che esegue una operazione di "Fusione" conosciuta anche come "Unione":

def fuse_obj(name, obj_0, obj_1):
    """Fuse two objects."""
    obj = DOC.addObject("Part::Fuse", name)
    obj.Base = obj_0
    obj.Tool = obj_1
    obj.Refine = True
    DOC.recompute()

    return obj

Anche qui nulla di eccezionale, notate comunque l'uniformità nel codice della funzione di scrittura; Questo approccio è molto più lineare di quello usato in molti altri Tutorial, aiuta molto ad incrementare la leggibilità del codice e anche quando si vuole fare copia e incolla.

Usiamo ora queste geometrie, cancellate le linee di codice dopo # objects definition e inserite queste linee:

# objects definition

obj = my_box("test_cube", 5, 5, 5)

obj1 = my_cyl("test_cyl", 360, 2, 10)

fuse_obj("Fusion", obj, obj1)

set_view()

Lanciate lo script con la freccia verde e vedrete nella vista 3D, qualcosa che assomiglia all'immagine qui sotto:

Cubo and cilindro

Posizionamento

Il concetto di Posizionamento è relativamente complesso, potete vedere Tutorial Aeroplano per una spiegazione più approfondita.

In genere abbiamo bisogno di posizionare una geometria rispetto ad un'altra geometria, cosa abbastanza frequente quando si creano geometrie complesse, il modo più comodo è quello di usare la proprietà Placement della geometria.

FreeCAD offre un'ampia scelta di modi con cui specificare questa proprietà, molto dipende anche dalle conoscenze di fondo dell'utente, quella più facile da comprendere è quella spiegata nel Tutorial citato, usa una particolare definizione della porzione Rotation all'interno della definizione di Placement.

FreeCAD.Placement(Vector(0, 0, 0), FreeCAD.Rotation(10, 20, 30), Vector(0, 0, 0))

Comunque al di sopra di ogni ulteriore considerazione, una cosa è cruciale, il concetto di "punto di riferimento" della geometria. In altri termini, il punto dal quale l'oggetto viene costruito da parte di FreeCAD, riportiamo in questa tabella, copiata direttamente da Placement:

Oggetto Punto di riferimento
Part.Box vertice sinistro (minx), anteriore (miny), inferiore (minz).
Parte.Sfera centro della sfera
Part.Cilindro centro della faccia inferiore
Part.Cono centro della faccia inferiore (o apice se il raggio inferiore è 0)
Part.Toro centro del toro
Funzioni derivate dagli schizzi La caratteristica eredita la posizione dello schizzo sottostante. Gli schizzi iniziano sempre con Posizione = (0, 0, 0). Questa posizione corrisponde all'origine nello schizzo.

Questa informazione va tenuta ben presente specie quando si applica una rotazione.

Alcuni esempi possono aiutare, eliminare la funzione my_box e tutte le righe dopo la funzione my_cyl e aggiungere il codice seguente dopo la funzione my_cyl:

def my_sphere(name, rad):
    """Create a Sphere."""
    obj = DOC.addObject("Part::Sphere", name)
    obj.Radius = rad

    DOC.recompute()

    return obj

def my_box2(name, len, wid, hei, cent=False, off_z=0):
    """Create a box with an optional z offset."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    if cent is True:
        pos = Vector(len * -0.5, wid * -0.5, off_z)
    else:
        pos = Vector(0, 0, off_z)

    obj_b.Placement = Placement(pos, ROT0, VEC0)

    DOC.recompute()

    return obj_b

def mfuse_obj(name, objs):
    """Fuse multiple objects."""
    obj = DOC.addObject("Part::MultiFuse", name)
    obj.Shapes = objs
    obj.Refine = True
    DOC.recompute()

    return obj

def airplane():
    """Create an airplane shaped solid."""
    fuselage_length = 30
    fuselage_diameter = 5
    wing_span = fuselage_length * 1.75
    wing_width = 7.5
    wing_thickness = 1.5
    tail_height = fuselage_diameter * 3.0
    tail_position = fuselage_length * 0.70
    tail_offset = tail_position - (wing_width * 0.5)

    obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)

    obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)

    obj3 = my_sphere("nose", fuselage_diameter)
    obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)

    obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
    obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)

    objs = (obj1, obj2, obj3, obj4)

    obj = mfuse_obj("airplane", objs)
    obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))

    DOC.recompute()

    return obj

# objects definition

airplane()

set_view()

Illustriamo meglio alcuni punti del codice:

Esempio Airplane
Airplane ruotato
Proprietà Placement

Potete facilmente notare che l'airplane ruota attorno al suo "baricentro" detto anche "centro di gravità", che ho fissato nel centro delle ali, una posizione abbastanza "naturale", potete comunque piazzarlo dove più vi aggrada o vi serve.

Il primo Vector(0,0,0) è il vettore di Traslazione (o di posizionamento), che qui non abbiamo usato, però se sostituite la riga airplane() con le linee seguenti:

obj_f = airplane()

print(obj_F.Placement)

Potete leggere nella finestra Report questo testo:

Placement [Pos=(0, -21, 21), Yaw-Pitch-Roll=(0, 0, -90)]

Cosa è successo?

FreeCAD ha "tradotto" il posizionamento passato con Vector(0, 0, 0), FreeCAD.Rotation(0, 0, -90), Vector(0, 0, tail_position), che specificava tre componenti Translazione, Rotazione e centro di rotazione nel suo valore "interno" che possiede solo due componenti, Translazione e Rotazione.

si può facilmente visualizzare il valore di tail_position utilizzando un'istruzione print nella funzione airplane() e vedere che è:

tail_position = 21.0

in parole parole, il centro di rotazione della geometria è posizionato a Vector(0, 0, 21), ma non è mostrato attraverso l'interfaccia grafica nella vista Dati, può essere specificato come valore nella proprietà Placement, ma non può essere facilmente recuperato.

Questo è il significato dell'aggettivo "delicato" che ho usato precedentemente nel testo per definire la proprietà Placement.


Questo è l'esempio di codice completo con una docstring di script decente che segue la convenzione sulle docstring di Google:

"""Sample code.

Filename:
   airplane.py

Author:
    Dormeletti Carlo (onekk)

Version:
    1.0

License:
    Creative Commons Attribution 3.0

Summary:
    This is sample code written for a FreeCAD Wiki page.
    It creates an airplane shaped solid using standard "Part WB" shapes.

"""

import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui

DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)

ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)

# Helper function

def set_view():
    """Rearrange View."""
    if not FreeCAD.GuiUp:
        return
    doc = FreeCADGui.ActiveDocument
    if doc is None:
        return
    view = doc.ActiveView
    if view is None:
        return
    # Check if the view is a 3D view:
    if not hasattr(view, "getSceneGraph"):
        return
    view.viewAxometric()
    view.fitAll()

# Script functions

def my_cyl(name, ang, rad, hei):
    """Create a Cylinder."""
    obj = DOC.addObject("Part::Cylinder", name)
    obj.Angle = ang
    obj.Radius = rad
    obj.Height = hei

    DOC.recompute()

    return obj

def my_sphere(name, rad):
    """Create a Sphere."""
    obj = DOC.addObject("Part::Sphere", name)
    obj.Radius = rad

    DOC.recompute()

    return obj

def my_box2(name, len, wid, hei, cent=False, off_z=0):
    """Create a box with an optional z offset."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    if cent is True:
        pos = Vector(len * -0.5, wid * -0.5, off_z)
    else:
        pos = Vector(0, 0, off_z)

    obj_b.Placement = Placement(pos, ROT0, VEC0)

    DOC.recompute()

    return obj_b

def mfuse_obj(name, objs):
    """Fuse multiple objects."""
    obj = DOC.addObject("Part::MultiFuse", name)
    obj.Shapes = objs
    obj.Refine = True
    DOC.recompute()

    return obj

def airplane():
    """Create an airplane shaped solid."""
    fuselage_length = 30
    fuselage_diameter = 5
    wing_span = fuselage_length * 1.75
    wing_width = 7.5
    wing_thickness = 1.5
    tail_height = fuselage_diameter * 3.0
    tail_position = fuselage_length * 0.70
    tail_offset = tail_position - (wing_width * 0.5)

    obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)

    obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)

    obj3 = my_sphere("nose", fuselage_diameter)
    obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)

    obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
    obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)

    objs = (obj1, obj2, obj3, obj4)

    obj = mfuse_obj("airplane", objs)
    obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))

    DOC.recompute()

    return obj

# objects definition

airplane()

set_view()